package org.groovyflow.db;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.groovyflow.util.IgnoreCaseSet;
import org.groovyflow.util.IgnoreCaseMap;
import org.groovyflow.GroovyFlowContext;

/**
 * A work in progress.
 * @author Chuck Tassoni
 * With thanks to Rene de Bloois for his help on dynamically creating properties 
 */
//TODO ??Might store null values as NULL so that we can throw an exception when
//someone references a value that's not valid.  If we do so, then we'll need
//to be smart enough to convert NULL to null when someone retrieves it and
//when we write to the database.  Would also need to convert null to NULL 
//when going the opposite direction. Probably no longer needed??
//Also see note/ActiveRecordTodo.txt
public class ActiveRecord implements java.io.Serializable{
	//Table values
	protected Map values = new IgnoreCaseMap()
	//Non-table values that occurred in the sql that populated this object
	protected Map extValues = new IgnoreCaseMap()
	//Maybe I should just rely on id and not worry about existsInDb.  
	//I recall there was some reason originally I didn't want to rely on id.
	private boolean existsInDb = false
	
	protected Map meta
	
	//In fact, this set ends up including everyting that's passed into
	//svalues() map.  Which means that they may end up including join table
	//columns, and will certainly end up including columns that exist
	//in the activeRecords corresponding table.  In addition, a subclass
	//may wish to augment this set in its constructor, so that it can
	//add properties that can be used even prior to sValues() being called.
	public Set extraCalls = new IgnoreCaseSet()
	
	public ActiveRecord(){
		//println 'No arg constructor of ActiveRecord called'
		gMetaData()
	}
	
	public ActiveRecord(Map map){
		//println 'Map constructor of ActiveRecord called'
		this.sValues(map)
	}

	//TODO  Perhaps in prod need  to run this by instantaiting each ActiveRecord.
	//Otherwise we probably need to syncrhonize part or all of this.
	def gMetaData(){
		if(meta != null)
			return meta
		//println 'attempting to find metaData'
		meta = gSqlDao().getTableCache().get(gTableName())
		//println "metaData at first is " + meta
		if(meta == null){
			//println 'metaData was not there, so now calling findTableMetaData'
			DbHelp.inTrx( {gSqlDao().findTableMetaData(this)} );
		}
		meta = gSqlDao().getTableCache().get(gTableName())
		//println 'metaData that gMetaData is returning is ' + meta
		return meta;
	}
	
	public SqlDao gSqlDao(){
		return (SqlDao) GroovyFlowContext.get("sqlDao");
	}	
	
	protected String idName(){
		return "id";
	}
	
	public final String gIdName(){
		return idName().toLowerCase()
	}
	
	public String gTableName(){
		return this.getClass().getSimpleName();
	}

	public String gPKSequenceName(){
		return this.gTableName() + "_seq";
	}
	
	public String gVersionFieldName(){
		return null;
	}
	
	public Set gFieldsToExclude(){
		return new HashSet();
	}
	
	boolean gExistsInDb(){
		return this.existsInDb
	}
	
	void sExistsInDb(tf){
		this.existsInDb = tf
	}

	def propertyMissing(String name){
		 //println 'propertyMissing get called for ' + name
		 registerProperty(name) 
         return this."$name" 
	 }

	 def propertyMissing(String name, value){
		 //println 'propertyMissing set called for ' + name
		 registerProperty(name) 
         this."$name" = value 
	 }
	 
	 def registerProperty(String name){
		 String key = name
		 def tableColumnNamesButNotId = gMetaData().get(SqlDao.COLUMN_NAMES) 
         String methodSuffix = key[0].toUpperCase() + key.substring(1)
         key = key.toLowerCase()
         
         if(!isValidProperty(key))
        	 throw new MissingPropertyException(name, this.getClass())
         
      if(tableColumnNamesButNotId.contains(key) || gIdName() == key){
          metaClass."get${methodSuffix}" = { -> return delegate.values[ key ] } 
          metaClass."set${methodSuffix}" = { Object value ->  delegate.values[ key ] = value } 
        }else if("id" == key){
             metaClass."get${methodSuffix}" = { -> return delegate.values[ gIdName() ] }
             metaClass."set${methodSuffix}" = { Object value ->  delegate.values[ gIdName() ] = value } 
            //println "Created methods " + "get${methodSuffix}" + "and set${methodSuffix}"
        }else{
           metaClass."get${methodSuffix}" = { -> return delegate.extValues[ key ] }    
           metaClass."set${methodSuffix}" = { Object value ->  delegate.extValues[ key ] = value } 
       }    

	 }
	 
     //Default is to throw MethodMissingException
     def methodMissingForNonSetGet(String name, args){
        throw new MissingMethodException(name, this.getClass(), args)
     }  
     
     def methodMissing(String name, args){
         def key = null
         if(name.startsWith("get"))
              key = name - "get"
         if(name.startsWith("set"))
              key = name - "set"
         //Give subclass a chance to do something 
         //with calls that are not setters or getters
         if(key == null)
            methodMissingForNonSetGet(name, args)
         key = key.toLowerCase()
         
         //println 'methodMissing called for key = ' + key

        if(isValidProperty(key)){
             if(name.startsWith("get")){
            	 return propertyMissing(key)
             }else if(name.startwWith("set")){
            	 return propertyMissing(key, args[0])
            }
             else throw new MissingMethodException(name, this.getClass(), args)
        }
        else{ //We have a getter/setter that's refering 
              //to a variable we don't know about
            throw new MissingMethodException(name, this.getClass(), args)
        }
       
    }   
     
    private boolean isValidProperty(String key){
    	return this.extraCalls.contains(key) || gMetaData().get(SqlDao.COLUMN_NAMES).contains(key) || gIdName() == key || "id" == key
    }
     
	
	def sValues(Map map){
	    def tableColumnNamesButNotId = gMetaData().get(SqlDao.COLUMN_NAMES)
	    //The names in tableColumnNamesButNotId also don't include any fields we declared to be excluded.
	    
	    //We add these to extraCalls because they might include read-only join tables.
	    //In fact, this almost always (maybe always) means that extraCalls will end up containing
	    //the names of all the allowable properties.
	    map.keySet().each{
			this.extraCalls << it.toLowerCase()
		}
	    
	    def idName = gIdName()
	    //Would be nice to just add the idName to the columns collection, but by doing so
	    //we'd add it to the values in the cache as well.  And we don't want to require 
	    //the cache to clone the columnNames collection on each get.
	    
	    map.each{key, value ->
	        key = key.toLowerCase()
	    	if(tableColumnNamesButNotId.contains(key) || idName == key)
	    		values[key] = value
	    	else
	    		extValues[key] = value	
	    }
		this
	}
	
	Map gValues(){
		values
	}
	
	def save(){
		DbHelp.inTrx({
			beforeSave()
			if(gExistsInDb())
				gSqlDao().doActiveRecordUpdate(this.gValues(), this)
			else{
				gSqlDao().doActiveRecordInsert(this.gValues(), this)
				sExistsInDb(true)
			}
		})
	}
	
	def beforeSave(){
		
	}
	
	def findById(id){
		 findByWhere(gIdName() + " = ? ", [id])[0]
	}
	
	//TODO  Should we discourage this simply by not having it?
	def findAll(){
		findBySql("select * from " + gTableName(), [])
	}
	
	def findByWhere(String whereAndOrder, List params){
	    findBySql("select * from " + gTableName() + " where " + whereAndOrder, params)
	}	
	
	/**
	* Be sure to select all the fields that you didn't mark as excluded.
	* You can add fields from other tables too-- of course, only the
	* fields belonging to the Record's table will actually be saved.
	*/
	def findBySql(String theSql, List params){
		def clos = { 
			def listOfMaps = gSqlDao().doSearch(theSql, params)
			def records = []
			
			def className = this.getClass().getName()
			//Inside conditionally-compiled code simple Class.forName doesn't work
			//In fact, now that we're in a closure, getClass().getClassLoader() doesn't work.
			//We need to do delegate.getClass().getClassLoader()
            //That's because, while delegate and this show the same classname,
            //they're not the same object, and I suppose they have diffent classLoaders.
			def clazz = Class.forName(className, true, delegate.getClass().getClassLoader())
			listOfMaps.each{ m ->
			    def one = clazz.newInstance()
				one.sValues(m)
				one.sExistsInDb(true)
				records << one
			}
			return records	
		}
		return DbHelp.inTrx(clos)
	}
	
	def delete(){
		def sql = "delete from " + gTableName() + " where " + gIdName() + " =?"
		DbHelp.inTrx({
			gSqlDao().updateDelOrInsert(sql, [delegate.id]) 
			Map values = delegate.gValues()
			values[gIdName()] = null
			delegate.sValues(values)
			sExistsInDb(false)
		})				
	}


	
}
